package cn.dansj.swagger.config;

import cn.dansj.commonutils.annotation.AbstractBeanMethodFactoryPointcutAdvisor;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.http.HttpServletRequest;

import cn.dansj.commonutils.annotation.request.EnableIntensifyRequest;
import cn.dansj.swagger.annotation.ApiResponse;
import cn.dansj.swagger.annotation.ApiResponseProperty;
import cn.dansj.swagger.annotation.StartPage;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore;
import springfox.documentation.oas.mappers.ServiceModelToOpenApiMapper;
import springfox.documentation.oas.web.OpenApiControllerWebMvc;
import springfox.documentation.oas.web.WebMvcOpenApiTransformationFilter;
import springfox.documentation.service.Documentation;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.DocumentationCache;
import springfox.documentation.spring.web.json.Json;
import springfox.documentation.spring.web.json.JsonSerializer;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import org.springframework.util.StringUtils;

import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
import static java.util.Optional.*;
import static springfox.documentation.schema.ClassSupport.*;

@Configuration
@Primary
@EnableIntensifyRequest
public class SwaggerConfig extends AbstractBeanMethodFactoryPointcutAdvisor implements SwaggerResourcesProvider, ApplicationContextAware {
    private static final List<String> startPageUrl = new ArrayList<>();
    private static final JSONObject apiResponseMap = new JSONObject();
    private final String swagger1Url;
    private final String swagger2Url;
    private final String oas3Url;

    private boolean oas3Available;
    private boolean swagger1Available;
    private boolean swagger2Available;

    private final DocumentationCache documentationCache;
    private final boolean oas3DocketsPresent;
    private final boolean swagger2DocketsPresent;

    public static List<String> getStartPageUrl() {
        return startPageUrl;
    }

    public static JSONObject getApiResponseMap() {
        return apiResponseMap;
    }

    @Autowired
    public SwaggerConfig(Environment environment, DocumentationCache documentationCache, DocumentationPluginsManager pluginsManager) {
        oas3DocketsPresent = pluginsManager.documentationPlugins().stream().anyMatch(d -> d.supports(DocumentationType.OAS_30));
        swagger2DocketsPresent = pluginsManager.documentationPlugins().stream().anyMatch(d -> d.supports(DocumentationType.SWAGGER_2));
        swagger1Url = environment.getProperty("springfox.documentation.swagger.v1.path", "/api-docs");
        swagger2Url = fixup(environment.getProperty("springfox.documentation.swagger.v2.path", "/v2/api-docs"));
        oas3Url = fixup(environment.getProperty("springfox.documentation.open-api.v3.path", "/v9/api-docs"));
        this.documentationCache = documentationCache;
    }

    private String fixup(String path) {
        if (StringUtils.isEmpty(path) || "/".equals(path) || "//".equals(path)) {
            return "/";
        }
        return StringUtils.trimTrailingCharacter(path.replace("//", "/"), '/');
    }

    @Override
    public List<SwaggerResource> get() {
        List<SwaggerResource> resources = new ArrayList<>();

        for (Map.Entry<String, Documentation> entry : documentationCache.all().entrySet()) {
            String swaggerGroup = entry.getKey();
            if (swagger1Available) {
                SwaggerResource swaggerResource = resource(swaggerGroup, swagger1Url);
                swaggerResource.setSwaggerVersion("1.2");
                resources.add(swaggerResource);
            }

            if (swagger2Available) {
                SwaggerResource swaggerResource = resource(swaggerGroup, swagger2Url);
                swaggerResource.setSwaggerVersion("2.0");
                resources.add(swaggerResource);
            }

            if (oas3Available) {
                SwaggerResource swaggerResource = resource(swaggerGroup, oas3Url);
                swaggerResource.setSwaggerVersion("3.0.3");
                resources.add(swaggerResource);
            }
        }
        Collections.sort(resources);
        return resources;
    }

    private SwaggerResource resource(String swaggerGroup, String baseUrl) {
        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(swaggerGroup);
        swaggerResource.setUrl(swaggerLocation(baseUrl, swaggerGroup));
        return swaggerResource;
    }

    private String swaggerLocation(String swaggerUrl, String swaggerGroup) {
        String base = of(swaggerUrl).get();
        if (Docket.DEFAULT_GROUP_NAME.equals(swaggerGroup)) {
            return base;
        }
        return base + "?group=" + swaggerGroup;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ClassLoader classLoader = applicationContext.getClassLoader();
        swagger1Available = classByName("springfox.documentation.swagger1.web.Swagger1Controller", classLoader).isPresent();
        swagger2Available = (classByName("springfox.documentation.swagger2.web.Swagger2ControllerWebFlux", classLoader).isPresent()
                || classByName("springfox.documentation.swagger2.web.Swagger2ControllerWebMvc", classLoader).isPresent()) && swagger2DocketsPresent;
        oas3Available = (classByName("springfox.documentation.oas.web.OpenApiControllerWebFlux", classLoader).isPresent()
                || classByName("springfox.documentation.oas.web.OpenApiControllerWebMvc", classLoader).isPresent()) && oas3DocketsPresent;
    }

    @Override
    public boolean match(Method method, Class<?> targetClass) {
        if (AnnotatedElementUtils.hasAnnotation(method, StartPage.class)) {
            final RequestMapping getMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
            final RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(targetClass, RequestMapping.class);
            if (getMapping == null) return false;
            if (requestMapping != null) {
                for (String pUrl : requestMapping.value()) {
                    for (String sUrl : getMapping.value()) {
                        for (RequestMethod requestMethod : getMapping.method()) {
                            startPageUrl.add(requestMethod.name() + "@" + pUrl + sUrl);
                        }
                    }
                }
            } else {
                for (String url : getMapping.value()) {
                    for (RequestMethod requestMethod : getMapping.method()) {
                        startPageUrl.add(requestMethod.name() + "@" + url);
                    }
                }
            }
        }

        if (AnnotatedElementUtils.hasAnnotation(method, ApiResponse.class)) {
            final RequestMapping getMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
            final RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(targetClass, RequestMapping.class);
            if (getMapping == null) return false;
            if (requestMapping != null) {
                for (String pUrl : requestMapping.value()) {
                    for (String sUrl : getMapping.value()) {
                        for (RequestMethod requestMethod : getMapping.method()) {
                            addApiResponse(method, requestMethod.name() + "@" + pUrl + sUrl);
                        }
                    }
                }
            } else {
                for (String url : getMapping.value()) {
                    for (RequestMethod requestMethod : getMapping.method()) {
                        addApiResponse(method, requestMethod.name() + "@" + url);
                    }
                }
            }
        }
        return false;
    }

    private void addApiResponse(Method method, String url) {
        final ApiResponse apiResponse = AnnotatedElementUtils.findMergedAnnotation(method, ApiResponse.class);
        List<JSONObject> responsePropertyList = new ArrayList<>();
        for (ApiResponseProperty apiResponseProperty : apiResponse.value()) {
            JSONObject responseProperty = new JSONObject();
            responseProperty.put("name", apiResponseProperty.name());
            responseProperty.put("value", apiResponseProperty.value());
            responseProperty.put("type", apiResponseProperty.type());
            responsePropertyList.add(responseProperty);
        }
        for (String property : apiResponse.property()) {
            final String[] split = property.replaceAll("\\s+", " ").split(" ");
            int size = split.length;
            if ((size & 1) == 1) {
                // 如果number不为偶数, 长度减1, 对应的值默认为null
                size -= 1;
            }
            int keyIndex = 0;
            int valueIndex = 1;
            while (size >= valueIndex) {
                JSONObject responseProperty = new JSONObject();
                responseProperty.put("name", split[keyIndex]);
                responseProperty.put("value", split[valueIndex]);
                responseProperty.put("type", null);
                keyIndex += 2;
                valueIndex += 2;
                responsePropertyList.add(responseProperty);
            }
        }
        apiResponseMap.put(url, responsePropertyList);
    }

    @Override
    public Object execute(MethodInvocation invocation) throws Throwable {
        return invocation.proceed();
    }

    @Controller
    @ApiIgnore
    public static class DocumentController {
        @GetMapping("/swagger.html")
        public String swagger() {
            return "index.html";
        }
    }

    @ApiIgnore
    @RestController
    @RequestMapping("/v9/api-docs")
    public static class OpenApiController extends OpenApiControllerWebMvc {
        private final JsonSerializer jsonSerializer;
        private static final List<JSONObject> pageParameter = pageParameter();
        private static final List<JSONObject> sortParameter = sortParameter();
        private static final ConcurrentMap<String, ResponseEntity<Json>> cache = new ConcurrentHashMap<>();

        public OpenApiController(DocumentationCache documentationCache, ServiceModelToOpenApiMapper mapper, JsonSerializer jsonSerializer, PluginRegistry<WebMvcOpenApiTransformationFilter, DocumentationType> transformations) {
            super(documentationCache, mapper, jsonSerializer, transformations);
            this.jsonSerializer = jsonSerializer;
        }

        @GetMapping(produces = {APPLICATION_JSON_VALUE, "application/hal+json"})
        public ResponseEntity<Json> getDocumentation(@RequestParam(value = "group", required = false) String group, HttpServletRequest servletRequest) {
            final String groupKey = group == null ? "default" : group;
            final ResponseEntity<Json> cacheResponse = cache.get(groupKey);
            if (cacheResponse != null) return cacheResponse;
            final ResponseEntity<Json> documentation = super.getDocumentation(group, servletRequest);
            if (!documentation.getStatusCode().equals(HttpStatus.valueOf(200)) || documentation.getBody() == null) {
                return documentation;
            }
            final JSONObject jsonObject = JSON.parseObject(documentation.getBody().value());
            final JSONObject path = jsonObject.getJSONObject("paths");
            final JSONObject newPath = new JSONObject();
            final JSONObject apiResponseMap = getApiResponseMap();
            for (String url : path.keySet()) {
                if (url.startsWith("/actuator") || url.startsWith("/error")) {
                    continue;
                }
                final JSONObject pathObject = path.getJSONObject(url);
                for (String method : pathObject.keySet()) {
                    final JSONObject methodInfo = pathObject.getJSONObject(method);
                    if (methodInfo == null) {
                        continue;
                    }
                    if (apiResponseMap.containsKey(method.toUpperCase() + "@" + url)) {
                        methodInfo.getJSONObject("responses").put("property", apiResponseMap.get(method.toUpperCase() + "@" + url));
                    }
                    addSortParameter(getParameters(methodInfo));
                    if (getStartPageUrl().contains(method.toUpperCase() + "@" + url))
                        addPageParameter(getParameters(methodInfo));
                }
                newPath.put(url, pathObject);
            }
            jsonObject.put("paths", newPath);

            jsonObject.getJSONArray("tags").removeIf(tag -> {
                final JSONObject tagObject = (JSONObject) tag;
                return tagObject.getString("name").equals("basic-error-controller") || tagObject.getString("name").equals("operation-handler") || tagObject.getString("name").equals("web-mvc-links-handler");
            });
            final ResponseEntity<Json> response = new ResponseEntity<>(jsonSerializer.toJson(jsonObject), HttpStatus.OK);
            cache.put(groupKey, response);
            return response;
        }

        private JSONArray getParameters(JSONObject get) {
            final JSONArray parameters = get.getJSONArray("parameters");
            if (parameters == null) {
                get.put("parameters", new JSONArray());
            }
            return get.getJSONArray("parameters");
        }

        private static List<JSONObject> pageParameter() {
            JSONObject pageSize = new JSONObject();
            pageSize.put("name", "pageSize");
            pageSize.put("in", "query");
            pageSize.put("description", "每页显示条数, 不为空开启分页");
            pageSize.put("required", false);
            pageSize.put("style", "form");
            pageSize.put("schema", new JSONObject().fluentPut("type", "integer"));
            JSONObject pageNum = new JSONObject();
            pageNum.put("name", "pageNum");
            pageNum.put("in", "query");
            pageNum.put("description", "当前页码, 不为空开启分页");
            pageNum.put("required", false);
            pageNum.put("style", "form");
            pageNum.put("schema", new JSONObject().fluentPut("type", "integer"));
            List<JSONObject> parameter = new ArrayList<>();
            parameter.add(pageNum);
            parameter.add(pageSize);
            return parameter;
        }

        private static List<JSONObject> sortParameter() {
            JSONObject sortBy = new JSONObject();
            sortBy.put("name", "sortBy");
            sortBy.put("in", "query");
            sortBy.put("description", "排序参数,内容填写返回的字段名称, 不为空开启排序");
            sortBy.put("required", false);
            sortBy.put("style", "form");
            sortBy.put("schema", new JSONObject().fluentPut("type", "string"));
            JSONObject sortType = new JSONObject();
            sortType.put("name", "sortType");
            sortType.put("in", "query");
            sortType.put("description", "排序方式, asc升序 desc降序, 默认降序");
            sortType.put("required", false);
            sortType.put("style", "form");
            sortType.put("schema", new JSONObject().fluentPut("type", "string"));
            List<JSONObject> parameter = new ArrayList<>();
            parameter.add(sortBy);
            parameter.add(sortType);
            return parameter;
        }

        private void addPageParameter(JSONArray parameters) {
            List<Object> params = new ArrayList<>(pageParameter);
            params.addAll(parameters.toJavaList(Object.class));
            parameters.clear();
            parameters.addAll(params);
        }

        private void addSortParameter(JSONArray parameters) {
            List<Object> params = new ArrayList<>(sortParameter);
            params.addAll(parameters.toJavaList(Object.class));
            parameters.clear();
            parameters.addAll(params);
        }
    }
}
